/*
 *  Copyright 2010 mazhao.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package shopping.commons;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;
import java.util.List;

/**
 * @author mazhao
 */
public class ModelObject implements java.io.Serializable {
    //-------------------------------------------
    // log objects
    //-------------------------------------------

    /** for subclass instance*/
    protected Log log = LogFactory.getLog(this.getClass());

    /** for subclass */
    protected static Log staticLog = LogFactory.getLog(ModelObject.class);

    //-------------------------------------------
    // for common Object methods
    //-------------------------------------------

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }

    //-------------------------------------------
    // for common CRUD operations
    //-------------------------------------------
    private String operationType = BaseAction.TYPE_UPDATE;

    public String getOperationType() {
        return operationType;
    }

    public void setOperationType(String operationType) {
        this.operationType = operationType;
    }


    //-------------------------------------------
    // for persistence methods common
    //-------------------------------------------

    /**
     * get class name
     * for using with namespace of mapper
     * <mapper namespace="shopping.model.Catalog">
     *
     * @return class name such as shopping.model.Catalog
     */
    protected String getClassName() {
        String cn = this.getClass().getName();
        return cn;
    }

    /**
     * get caller method name.
     * the name of the caller method which call getCallerMethodName() method.
     * @return the name of the caller method
     */
    protected String getCallerMethodName() {
        StackTraceElement[] stes = new Throwable().getStackTrace();
        if (stes.length > 2) {
            String methodName = stes[1].getMethodName();
            return methodName;
        } else {
            log.warn("can not get caller method for class " + this.getClass());
            return "";
        }
    }

    /** myBatis's configuration file */
    private static final String MY_BATIS_CONF = "MyBatisConfiguration.xml";

    /** myBatis's sql session factory  */
    private static SqlSessionFactory SQL_MAPPER = null;

    /**
     * read configuration fild and build sql session factory builder;
     */
    static {
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader(MY_BATIS_CONF);
            SQL_MAPPER = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException ex) {
            staticLog.error("can not load mybatis configuration file. the exception is " + ex);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException ex) {
                staticLog.warn("can not close mybatis configuration file. the exception is" + ex);
            }
        }
    }

    /**
     * get sql session for database operations.
     * @return
     */
    protected static SqlSession getSession() {
        return SQL_MAPPER.openSession();
    }

    //-------------------------------------------
    // for persistence methods specific
    //-------------------------------------------

    /** operated record count */
    protected int recordCount = 0;

    /**
     * get operated record count
     * @return
     */
    public int getRecordCount() {
        return recordCount;
    }

    /** sql session for database operations */
    protected transient SqlSession session;

    /** if auto commit */
    protected boolean autoTrans = true;

    /**
     * begin transaction
     */
    public synchronized void beginTrans() {
        autoTrans = false;
        session = getSession();
        log.debug("transaction begin");
    }

    /**
     * commit transaction
     */
    public synchronized void commit() {
        if (session != null) {
            session.commit();
            session.close();
            autoTrans = true;
            log.debug("commit transaction successfully");
        } else {
            log.error("please first connect then then beginTrans then commit ");
        }
    }

    /**
     * rollback transaction
     */
    public synchronized void rollback() {
        if (session != null) {
            session.rollback();
            session.close();
            autoTrans = true;
            log.debug("rollback transaction successfully");
        } else {
            log.error("please first connect then then beginTrans then rollback ");
        }
    }

    /**
     * save instance to database.
     * @return true for success, false else
     */
    public synchronized boolean save() {
        // excepted class.name.save
        String sqlKey = getClassName() + "." + getCallerMethodName();
        log.debug("sql key for save is " + sqlKey);

        if (autoTrans) {
            session = getSession();
        }

        recordCount = session.insert(sqlKey, this);
        log.debug("record count is " + recordCount);
        if (autoTrans) {
            session.commit();
            session.close();
        }
        return recordCount == 1;
    }

    /**
     * update instace to database.
     * <<< update this one or many according to the sql >>>
     * @return true for success, false else
     */
    public synchronized boolean update() {
        // excepted class.name.update
        String sqlKey = getClassName() + "." + getCallerMethodName();
        log.debug("the sql key for update is " + sqlKey);
        if (autoTrans) {
            session = getSession();
        }

        recordCount = session.update(sqlKey, this);
        log.debug("record count is " + recordCount);

        if (autoTrans) {
            session.commit();
            session.close();
        }
        return recordCount > 0;
    }

    /**
     * delete instance from database
     * <<< delete this one or many according to the sql >>>
     * @return true for success, false else
     */
    public synchronized boolean delete() {
        // expected class.name.delete
        String sqlKey = getClassName() + "." + getCallerMethodName();
        log.debug("the sql key for delete is " + sqlKey);
        if (autoTrans) {
            session = getSession();
        }

        recordCount = session.delete(sqlKey, this);
        log.debug("record count is " + recordCount);

        if (autoTrans) {
            session.commit();
            session.close();
        }
        return recordCount > 0;
    }

    /**
     * load the object by sql with id "MODEL_CLASS_NAME.load"
     * <<< load only one instance with primary key >>>
     * @return true if load successfully, false else.
     */
    public synchronized boolean load() {
        // excepted class.name.load
        String sqlKey = getClassName() + "." + getCallerMethodName();
        log.debug("the sql key for load is " + sqlKey);
        if (autoTrans) {
            session = getSession();
        }

        ModelObject mo = (ModelObject) session.selectOne(sqlKey, this);

        if (autoTrans) {
            session.close();
        }

        if (mo != null) {
            // copy object to self
            log.debug("find object to load " + mo);
            try {
                PropertyUtils.copyProperties(this, mo);
            } catch (Exception e) {
                log.error("can not copy properties for this object, source object " + mo + " target " + this);
            }
            recordCount = 1;
            log.debug("record count is " + recordCount);
            return true;
        } else {
            log.error("can not find object to load, load parameter is" + this);
            return false;
        }
    }

    /**
     * load many instance with the condition specified by this instance.
     * @return
     */
    public synchronized List<? extends ModelObject> loadMany() {
         // excepted class.name.load
        String sqlKey = getClassName() + "." + getCallerMethodName();
        log.debug("the sql key for loadMany is " + sqlKey);
        if (autoTrans) {
            session = getSession();
        }

        List<? extends ModelObject>  mos = session.selectList(sqlKey, this);
        recordCount = mos.size();

        if (autoTrans) {
            session.close();
        }

       return mos;
    }
}
